package com.badoo.mvicore.newspublishing

import com.atom.module.mvi.binder.middleware.config.MiddlewareConfiguration
import com.atom.module.mvi.binder.middleware.config.Middlewares
import com.atom.module.mvi.binder.middleware.config.WrappingCondition
import com.atom.module.mvi.core.consumer.middleware.ConsumerMiddleware
import com.atom.module.mvi.core.element.Actor
import com.atom.module.mvi.core.element.NewsPublisher
import com.atom.module.mvi.core.element.Reducer
import com.atom.module.mvi.core.feature.BaseFeature
import com.atom.module.mvi.core.feature.Feature
import com.badoo.mvicore.newspublishing.TestNews.News1
import com.badoo.mvicore.newspublishing.TestNews.News2
import com.badoo.mvicore.newspublishing.TestNews.News3
import com.badoo.mvicore.newspublishing.TestWish.Wish1
import com.badoo.mvicore.newspublishing.TestWish.Wish2
import com.badoo.mvicore.newspublishing.TestWish.Wish3
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.observers.TestObserver
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import kotlin.test.assertEquals

sealed class TestWish {
    object Wish1 : TestWish()
    object Wish2 : TestWish()
    object Wish3 : TestWish()
}

sealed class TestNews {
    object News1 : TestNews()
    object News2 : TestNews()
    object News3 : TestNews()
}

class Parameter(val middlewareConfiguration: MiddlewareConfiguration?) {
    override fun toString(): String = if (middlewareConfiguration != null) "with 3rd party middleware" else "without 3rd party middleware"
}

private fun <T : Any> createMiddlewareStub(consumer: Consumer<T>): ConsumerMiddleware<T> = object : ConsumerMiddleware<T>(consumer) {}

@RunWith(Parameterized::class)
class NewsPublishingTest(private val parameter: Parameter) {

    companion object {
        /**
         * The fact of using a wrapped news publisher or not shouldn't affect the news publishing logic.
         */
        @JvmStatic
        @Parameters(name = "{0}")
        fun parameters(): Iterable<Any?> = listOf<Any?>(
            // setup some middleware
            Parameter(
                MiddlewareConfiguration(
                    condition = WrappingCondition.Always,
                    factories = listOf(
                        { consumer -> createMiddlewareStub(consumer) }
                    )
                )
            ),

            // not using middleware
            Parameter(null)
        )
    }

    private lateinit var feature: Feature<TestWish, Any, TestNews>
    private lateinit var newsTestSubscriber: TestObserver<TestNews>

    @Before
    fun setUp() {
        parameter.middlewareConfiguration?.let {
            Middlewares.configurations.add(it)
        }
    }

    @After
    fun tearDown() {
        Middlewares.configurations.clear()
    }

    @Test
    fun `feature wo news publisher - emit wishes - no news produced`() {
        initializeFeatureWithNewsPublisher(null)

        listOf(Wish1, Wish2, Wish3).forEach(feature::accept)

        newsTestSubscriber.assertNoValues()
    }

    @Test
    fun `feature with news publisher which returns null - emit wishes - no news produced`() {
        initializeFeatureWithNewsPublisher { _, _, _ ->
            null
        }

        listOf(Wish1, Wish2, Wish3).forEach(feature::accept)

        newsTestSubscriber.assertNoValues()
    }

    @Test
    fun `feature with news publisher which returns 1 news - emit N wishes - N same news produced`() {
        initializeFeatureWithNewsPublisher { _, _, _ ->
            News1
        }

        listOf(Wish1, Wish2, Wish3).forEach(feature::accept)

        newsTestSubscriber.assertValues(News1, News1, News1)
    }

    @Test
    fun `feature with news publisher which returns different news - emit N wishes - N different news produced with a correct order`() {
        initializeFeatureWithNewsPublisher { action, _, _ ->
            when (action) {
                is Wish1 -> News1
                is Wish2 -> News2
                is Wish3 -> News3
                else -> null
            }
        }

        listOf(Wish3, Wish1, Wish2).forEach(feature::accept)

        newsTestSubscriber.assertValues(News3, News1, News2)
    }

    @Test
    fun `news publisher middleware, feature with news publisher - emit N wishes - N events propagated to news publisher middleware`() {
        val testMiddleware = setupTestMiddlewareConfigurationForNews()

        initializeFeatureWithNewsPublisher { action, _, _ ->
            when (action) {
                is Wish1 -> News1
                is Wish2 -> News2
                is Wish3 -> News3
                else -> null
            }
        }

        listOf(Wish3, Wish1, Wish2).forEach(feature::accept)

        with(argumentCaptor<Triple<TestWish, Any, Any>>()) {
            verify(testMiddleware, times(3)).onElement(any(), capture())
            assertEquals(listOf(Wish3, Wish1, Wish2), allValues.map { it.first })
        }
    }

    private fun initializeFeatureWithNewsPublisher(newsPublisher: NewsPublisher<Any, Any, Any, TestNews>?) {
        feature = BaseFeature(
            initialState = mock(),
            bootstrapper = null,
            wishToAction = { it },
            actor = mock<Actor<Any, Any, Any>>().also {
                whenever(it.invoke(any(), any())).then { Observable.just(mock<Any>()) }
            },
            reducer = mock<Reducer<Any, Any>>().also {
                whenever(it.invoke(any(), any())).then { mock() }
            },
            postProcessor = null,
            newsPublisher = newsPublisher
        )
        newsTestSubscriber = Observable.wrap(feature.news).test()
    }

    @Suppress("RedundantLambdaArrow")
    private fun setupTestMiddlewareConfigurationForNews(): ConsumerMiddleware<Triple<TestWish, Any, Any>> {
        val testMiddleware = spy(createMiddlewareStub(mock<Consumer<Triple<TestWish, Any, Any>>>()))

        Middlewares.configurations.add(
            MiddlewareConfiguration(
                condition = WrappingCondition.InstanceOf(NewsPublisher::class.java),
                factories = listOf({ _ -> testMiddleware })
            )
        )

        return testMiddleware
    }
}
